#include "coin2/exchange/base/log/latency_recorder.h"

namespace coin2::exchange::base::strategy_util {

void LatencyRecorder::Begin(coin::proto::LatencyProto::LatencyTag tag) {
  auto context_id = GetNextContextId();
  auto ptr = std::make_unique<coin::proto::LatencyProto>();
  ptr->set_name("life_of_signal");
  ptr->set_context_id(context_id);
  ptr->set_tag(tag);
  PutIntoCache(context_id, std::move(ptr));
}

void LatencyRecorder::End(int64_t context_id) {
  auto i = GetFromCache(context_id);
  if (i == m_map.end()) {
    return;
  }
  auto ele = i->second.first.get();
  auto latency_tag = ele->tag();
  if (latency_tag == coin::proto::LatencyProto::FEED_LATENCY ||
      latency_tag == coin::proto::LatencyProto::BOOK_FEED_LATENCY ||
      latency_tag == coin::proto::LatencyProto::TRADE_FEED_LATENCY) {
    auto curr_ts = GetTimestamp();
    if (prev_feed_latency_send_ts_.find(latency_tag) == prev_feed_latency_send_ts_.end() ||
        prev_feed_latency_send_ts_[latency_tag] + 10'000'000'000LL < curr_ts) {
      ele->set_done(true);
      prev_feed_latency_send_ts_[latency_tag] = curr_ts;
    }
  } else if (latency_tag == coin::proto::LatencyProto::ORDER_LATENCY) {
    ele->set_done(true);
  }
  WriteTelemetry(i);
}

void LatencyRecorder::SetTimeoutMark(
    const coin::proto::AccountRequestProto& acc_request, const std::string& tag) {
  Begin(coin::proto::LatencyProto::ORDER_LATENCY);
  auto context_id = GetCurrentContextId();
  SetProtoAndTime(
      context_id,
      coin::proto::LifeOfSignal::OG_REST_TIMEOUT_ERROR,
      GetTimestamp(),
      acc_request,
      tag);
  End(context_id);
}

void LatencyRecorder::SetBeforeSubmitMark(
    const OrderContext* const oc,
    const coin::proto::AccountRequestProto& acc_request) {
  oc->set_latency_context_id(GetCurrentContextId());
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::BEFORE_OG_ORDER_SUBMIT,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
}

void LatencyRecorder::SetAfterSubmitMark(
      const OrderContext* const oc,
      const coin::proto::AccountRequestProto& acc_request) {
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::AFTER_OG_ORDER_SUBMIT,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
  End(oc->latency_context_id());
}

void LatencyRecorder::SetBeforeCancelMark(
    const OrderContext* const oc,
    const coin::proto::AccountRequestProto& acc_request) {
  oc->set_latency_context_id(GetCurrentContextId());
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::BEFORE_OG_ORDER_CANCEL,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
}

void LatencyRecorder::SetAfterCancelMark(
    const OrderContext* const oc,
    const coin::proto::AccountRequestProto& acc_request) {
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::AFTER_OG_ORDER_CANCEL,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
  End(oc->latency_context_id());
}

void LatencyRecorder::SetBeforeAmendMark(
    const OrderContext* const oc,
    const coin::proto::AccountRequestProto& acc_request) {
  oc->set_latency_context_id(GetCurrentContextId());
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::BEFORE_OG_ORDER_AMEND,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
}

void LatencyRecorder::SetAfterAmendMark(
    const OrderContext* const oc,
    const coin::proto::AccountRequestProto& acc_request) {
  SetProtoAndTime(
      oc->latency_context_id(),
      coin::proto::LifeOfSignal::AFTER_OG_ORDER_AMEND,
      GetTimestamp(),
      acc_request,
      std::to_string(oc->proc_order_id()));
  End(oc->latency_context_id());
}

void LatencyRecorder::SetProtoAndTime(
    int64_t context_id,
    const coin::proto::LifeOfSignal& point_name,
    int64_t timestamp,
    const coin::proto::AccountRequestProto& account_request,
    const std::string& tag) {
  auto it = GetFromCache(context_id);
  if (it == m_map.end()) {
    LOG(INFO) << "[LatencyRecorder] No context id in Cache!!!"
              << context_id << " "  << coin::proto::LifeOfSignal_Name(point_name);
    return;
  }
  auto proto_ptr = it->second.first.get();
  proto_ptr->set_last_update_timestamp(timestamp);
  auto* mark_point = proto_ptr->add_mark_points();
  mark_point->set_point_name(point_name);
  *(mark_point->mutable_account_request()) = account_request;
  mark_point->set_context_id(context_id);
  mark_point->set_timestamp(timestamp);
  mark_point->set_tag(tag);
  if (point_name == coin::proto::LifeOfSignal::BEFORE_OG_ORDER_SUBMIT ||
      point_name == coin::proto::LifeOfSignal::BEFORE_OG_ORDER_AMEND ||
      point_name == coin::proto::LifeOfSignal::BEFORE_OG_ORDER_CANCEL) {
    proto_ptr->set_has_order_mark_point(true);
  }
}

void LatencyRecorder::WriteTps(const coin::proto::TpsProto& tps) {
  if (tps.window_size() <= 0) {
    LOG(INFO) << "Ignore TPS record!" << tps.DebugString();
    return;
  }
  if (!write_telemetry_) {
    LOG(ERROR) << "write_telemetry not set!";
  }

  telemetry_.Clear();
  telemetry_.set_type(coin::proto::TelemetryProto::TPS);
  telemetry_.mutable_tps()->MergeFrom(tps);
  DLOG(INFO) << "WriteTps: " << telemetry_.DebugString();
  write_telemetry_(telemetry_, true, GetCurrentTimestamp());
}

void LatencyRecorder::PutIntoCache(
    const key_type& key, std::unique_ptr<value_type> value) {
  typename map_type::iterator i = m_map.find(key);
  if (i == m_map.end()) {
    // insert item into the cache, but first check if it is full
    if (size() >= cap_) {
      // cache is full, evict the least recently used item
      bool evicted = Evict(true);
      if (!evicted) {
        LOG(INFO) << "[Force Evict latency of pending order]";
        CHECK(Evict());
      }
    }
    // insert the new item
    m_list.push_front(key);
    m_map[key] = std::make_pair(std::move(value), m_list.begin());
  }
}

LatencyRecorder::map_type::iterator LatencyRecorder::GetFromCache(
    const key_type& key) {
  // lookup value in the cache
  typename map_type::iterator i = m_map.find(key);
  if (i != m_map.end()) {
    // return the value, but first update its place in the most
    // recently used list
    typename list_type::iterator j = i->second.second;
    if (j != m_list.begin()) {
      // move item to the front of the most recently used list
      m_list.erase(j);
      m_list.push_front(key);

      // update iterator in map
      j = m_list.begin();
      m_map.at(key).second = j;
    }
  }
  // return the value
  return i;
}

void LatencyRecorder::WriteTelemetry(map_type::const_iterator i) {
  if (i != m_map.end()) {
    auto const latency_ptr = i->second.first.get();
    auto latency_tag = coin::proto::LatencyProto::LatencyTag_Name(latency_ptr->tag());
    if (latency_ptr->done()) {
      LOG(INFO) << "[Write Telemetry]";
      write_func_(*(latency_ptr), true, GetCurrentTimestamp());
    } else {
      LOG_FIRST_N(ERROR, 10)
          << "[Telemetry Not Ready] removing unwritten "
          << latency_tag
          << " telemetry due to size.\n"
          << "[Telemetry Not Ready] plz call missing End(context_id)";
    }
    std::string last_point_name = "";
    auto point_size = latency_ptr->mark_points_size();
    if (point_size > 0) {
      last_point_name = coin::proto::LifeOfSignal_Name(
          latency_ptr->mutable_mark_points()->at(point_size - 1).point_name());
    }
    DLOG(INFO) << "Evict latency. "
               << "latency_tag: " << latency_tag << " "
               << "context_id: " << latency_ptr->context_id() << " "
               << "done: " << latency_ptr->done() << " "
               << "last_point_name: " << last_point_name;    
    m_map.erase(i);
  }
}

bool LatencyRecorder::IsLatencyEvicted(const value_type* latency, bool ignore_pending_order) {
  bool evicted = true;
  if (latency->tag() == coin::proto::LatencyProto::ORDER_LATENCY &&
      latency->has_order_mark_point()) {
    if (ignore_pending_order) {
      if (GetTimestamp() - latency->last_update_timestamp() < 60e9) {
        evicted = false;
      } else {
        LOG(INFO) << "Evict expired pending order latency.";
      }
    }
  }
  return evicted;
}

bool LatencyRecorder::Evict(bool ignore_pending_order) {
  // evict item from the end of most recently used list
  typename list_type::reverse_iterator i = m_list.rbegin();
  bool evicted = false;
  while (i != m_list.rend()) {
    typename map_type::iterator j = m_map.find(*i);
    i++;
    if (j != m_map.end()) {
      evicted = IsLatencyEvicted(j->second.first.get(), ignore_pending_order);
      if (evicted) {
        WriteTelemetry(j);
        m_list.erase(i.base());
        break;
      }
    } else {
      auto fi = m_list.erase(i.base());
      i = std::make_reverse_iterator(fi);
    }
  }
  return evicted;
}

}  // namespace coin2::exchange::base::strategy_util
